Coverage Report

Created: 2026-08-07 16:19

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\cssh-rs\cssh-rs\cssh-rs-core\src\utils\config.rs
Line
Count
Source
1
//! Client and Daemon configuration structs.
2
3
use serde_derive::{Deserialize, Serialize};
4
use std::env;
5
use windows::Win32::System::Console::{
6
    BACKGROUND_BLUE, BACKGROUND_INTENSITY, BACKGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN,
7
    FOREGROUND_INTENSITY, FOREGROUND_RED,
8
};
9
10
/// Behavior when an arrow / `hjkl` keystroke would move the
11
/// enable/disable submenu's selection past the edge of the client grid.
12
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy)]
13
#[serde(rename_all = "snake_case")]
14
pub enum EdgeBehavior {
15
    /// Keep the current selection on edge keystrokes (default).
16
    Clamp,
17
    /// Wrap to the opposite edge of the same row (Left/Right) or column
18
    /// (Up/Down).
19
    Wrap,
20
}
21
22
impl Default for EdgeBehavior {
23
79
    fn default() -> Self {
24
79
        return EdgeBehavior::Clamp;
25
79
    }
26
}
27
28
/// Default console color applied when a client is in the
29
/// `Disabled` state.
30
///
31
/// Default-grey foreground (red+green+blue, no intensity) on a
32
/// `BACKGROUND_INTENSITY`-only background paints the window as light text on
33
/// a muted dark-grey background - a clear "this client is greyed out" cue
34
/// that stays visually distinct from the daemon's bright-red palette.
35
const DEFAULT_DISABLED_CONSOLE_COLOR: u16 =
36
    FOREGROUND_RED.0 | FOREGROUND_GREEN.0 | FOREGROUND_BLUE.0 | BACKGROUND_INTENSITY.0;
37
38
/// Default console color for the daemon's currently selected submenu
39
/// client: bright-white on blue, distinct from the daemon's bright-red
40
/// and the muted disabled palettes so it stands out at a glance.
41
const DEFAULT_HIGHLIGHTED_CONSOLE_COLOR: u16 = FOREGROUND_RED.0
42
    | FOREGROUND_GREEN.0
43
    | FOREGROUND_BLUE.0
44
    | FOREGROUND_INTENSITY.0
45
    | BACKGROUND_BLUE.0;
46
47
/// Placeholder for the `<username>@<host>` argument to the chosen SSH program.
48
const DEFAULT_USERNAME_HOST_PLACEHOLDER: &str = "{{USERNAME_AT_HOST}}";
49
50
/// Representation of the project configuration.
51
///
52
/// Includes subcommand specific configurations for `client` and `daemon` subcommands
53
/// as well es the cluster tags.
54
#[derive(Serialize, Deserialize, Default, PartialEq, Debug)]
55
pub struct Config {
56
    /// List of cluster tags.
57
    ///
58
    /// Includes the name of the cluster tag and a list of hostnames.
59
    pub clusters: Vec<Cluster>,
60
    /// Configuration relevant for the `client` subcommand.
61
    pub client: ClientConfig,
62
    /// Configuration relevant for the `daemon` subcommand.
63
    pub daemon: DaemonConfig,
64
}
65
66
/// Representation of the project configuration
67
/// where everything is optional.
68
///
69
/// Used to handle cases where only some or none of the configurations are present.
70
/// Enables backwards compatiblity with configuration files written by older versions.
71
#[derive(Serialize, Deserialize, Default)]
72
pub struct ConfigOpt {
73
    #[allow(missing_docs)]
74
    pub clusters: Option<Vec<Cluster>>,
75
    #[allow(missing_docs)]
76
    pub client: Option<ClientConfigOpt>,
77
    #[allow(missing_docs)]
78
    pub daemon: Option<DaemonConfigOpt>,
79
}
80
81
impl From<ConfigOpt> for Config {
82
    /// Unwraps the existing configuration values or applies the default.
83
16
    fn from(val: ConfigOpt) -> Self {
84
16
        return Config {
85
16
            clusters: val.clusters.unwrap_or_default(),
86
16
            client: val.client.unwrap_or_default().into(),
87
16
            daemon: val.daemon.unwrap_or_default().into(),
88
16
        };
89
16
    }
90
}
91
92
impl From<Config> for ConfigOpt {
93
    /// Wraps all configuration values as options.
94
1
    fn from(val: Config) -> Self {
95
1
        return ConfigOpt {
96
1
            clusters: Some(val.clusters),
97
1
            client: Some(val.client.into()),
98
1
            daemon: Some(val.daemon.into()),
99
1
        };
100
1
    }
101
}
102
103
/// Representation of a cluster tag.
104
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
105
pub struct Cluster {
106
    /// Name of the cluster tag, used to identify it.
107
    pub name: String,
108
    /// List of hostnames the cluster tag is an alias for.
109
    pub hosts: Vec<String>,
110
}
111
112
/// Representation of the `client` subcommand configurations.
113
#[derive(Serialize, Deserialize, PartialEq, Debug)]
114
pub struct ClientConfig {
115
    /// Full path to the SSH config.
116
    ///
117
    /// # Example
118
    ///
119
    /// `'C:\Users\<username>\.ssh\config'`
120
    pub ssh_config_path: String,
121
    /// Name of the program used to establish the SSH connection.
122
    ///
123
    /// Note specific to `cmd.exe`: a relayed Ctrl+C interrupts a running command
124
    /// but cannot reprint its idle prompt (which needs a real focused keypress).
125
    /// # Example
126
    ///
127
    /// `'ssh'`
128
    pub program: String,
129
    /// List of arguments provided to the program.
130
    ///
131
    /// Must include the `username_host_placeholder`.
132
    ///
133
    /// # Example
134
    ///
135
    /// `['-XY', '{{USERNAME_AT_HOST}}']`
136
    pub arguments: Vec<String>,
137
    /// Placeholder string used to inject `<user>@<host>` into the list of arguments.
138
    ///
139
    /// # Example
140
    ///
141
    /// `'{{USERNAME_AT_HOST}}'`
142
    pub username_host_placeholder: String,
143
    /// Controls back- and foreground colors of the client console window
144
    /// when the client is in the `Disabled` state.
145
    ///
146
    /// Uses the same encoding as [`DaemonConfig::console_color`].
147
    /// All [standard Windows color combinations][1] are available:
148
    ///
149
    /// FOREGROUND_BLUE:        1   \
150
    /// FOREGROUND_GREEN:       2   \
151
    /// FOREGROUND_RED:         4   \
152
    /// FOREGROUND_INTENSITY:   8   \
153
    /// BACKGROUND_BLUE:        16  \
154
    /// BACKGROUND_GREEN:       32  \
155
    /// BACKGROUND_RED:         64  \
156
    /// BACKGROUND_INTENSITY:   128 \
157
    ///
158
    /// # Example
159
    ///
160
    /// Default-grey font on muted dark-grey background:
161
    /// 4 + 2 + 1 + 128 = `135`
162
    ///
163
    /// [1]: https://learn.microsoft.com/en-us/windows/console/console-screen-buffers#character-attributes
164
    pub disabled_console_color: u16,
165
    /// Controls back- and foreground colors of the client console window
166
    /// while it is the currently selected window in the daemon's
167
    /// enable/disable submenu.
168
    ///
169
    /// Uses the same encoding as [`DaemonConfig::console_color`].
170
    /// All [standard Windows color combinations][1] are available:
171
    ///
172
    /// FOREGROUND_BLUE:        1   \
173
    /// FOREGROUND_GREEN:       2   \
174
    /// FOREGROUND_RED:         4   \
175
    /// FOREGROUND_INTENSITY:   8   \
176
    /// BACKGROUND_BLUE:        16  \
177
    /// BACKGROUND_GREEN:       32  \
178
    /// BACKGROUND_RED:         64  \
179
    /// BACKGROUND_INTENSITY:   128 \
180
    ///
181
    /// # Example
182
    ///
183
    /// Bright-white font on blue background:
184
    /// 4 + 2 + 1 + 8 + 16 = `31`
185
    ///
186
    /// [1]: https://learn.microsoft.com/en-us/windows/console/console-screen-buffers#character-attributes
187
    pub highlighted_console_color: u16,
188
}
189
190
impl Default for ClientConfig {
191
    /// Returns a sensible default `ClientConfig`.
192
    ///
193
    /// # Returns
194
    ///
195
    /// `ClientConfig` with the following values:
196
    /// * `ssh_config_path`             - `%USERPROFILE%\.ssh\config`
197
    /// * `program`                     - `ssh`
198
    /// * `arguments`                   - `-XY {{USERNAME_AT_HOST}}`
199
    /// * `username_host_placeholder`   - `{{USERNAME_AT_HOST}}`
200
    /// * `disabled_console_color`      - `135`
201
    /// * `highlighted_console_color`   - `31`
202
    ///
203
    /// Note: %USERPROFILE% actually is resolved by us, so the actual value
204
    ///       is whatever the environment variable at runtime points to.
205
81
    fn default() -> Self {
206
81
        return ClientConfig {
207
81
            ssh_config_path: format!("{}\\.ssh\\config", env::var("USERPROFILE").unwrap()),
208
81
            program: "ssh".to_string(),
209
81
            arguments: vec![
210
81
                "-XY".to_string(),
211
81
                DEFAULT_USERNAME_HOST_PLACEHOLDER.to_string(),
212
81
            ],
213
81
            username_host_placeholder: DEFAULT_USERNAME_HOST_PLACEHOLDER.to_string(),
214
81
            disabled_console_color: DEFAULT_DISABLED_CONSOLE_COLOR,
215
81
            highlighted_console_color: DEFAULT_HIGHLIGHTED_CONSOLE_COLOR,
216
81
        };
217
81
    }
218
}
219
220
/// Representation of the `client` subcommand configurations
221
/// where everything is optional.
222
#[derive(Serialize, Deserialize)]
223
pub struct ClientConfigOpt {
224
    #[allow(missing_docs)]
225
    pub ssh_config_path: Option<String>,
226
    #[allow(missing_docs)]
227
    pub program: Option<String>,
228
    #[allow(missing_docs)]
229
    pub arguments: Option<Vec<String>>,
230
    #[allow(missing_docs)]
231
    pub username_host_placeholder: Option<String>,
232
    #[allow(missing_docs)]
233
    pub disabled_console_color: Option<u16>,
234
    #[allow(missing_docs)]
235
    pub highlighted_console_color: Option<u16>,
236
}
237
238
impl Default for ClientConfigOpt {
239
13
    fn default() -> Self {
240
13
        return ClientConfig::default().into();
241
13
    }
242
}
243
244
impl From<ClientConfigOpt> for ClientConfig {
245
    /// Unwraps the existing configuration values or applies the default.
246
20
    fn from(val: ClientConfigOpt) -> Self {
247
20
        let default = ClientConfig::default();
248
20
        return ClientConfig {
249
20
            ssh_config_path: val.ssh_config_path.unwrap_or(default.ssh_config_path),
250
20
            program: val.program.unwrap_or(default.program),
251
20
            arguments: val.arguments.unwrap_or(default.arguments),
252
20
            username_host_placeholder: val
253
20
                .username_host_placeholder
254
20
                .unwrap_or(default.username_host_placeholder),
255
20
            disabled_console_color: val
256
20
                .disabled_console_color
257
20
                .unwrap_or(default.disabled_console_color),
258
20
            highlighted_console_color: val
259
20
                .highlighted_console_color
260
20
                .unwrap_or(default.highlighted_console_color),
261
20
        };
262
20
    }
263
}
264
265
impl From<ClientConfig> for ClientConfigOpt {
266
    /// Wraps all configuration values as options.
267
14
    fn from(val: ClientConfig) -> Self {
268
14
        return ClientConfigOpt {
269
14
            ssh_config_path: Some(val.ssh_config_path),
270
14
            program: Some(val.program),
271
14
            arguments: Some(val.arguments),
272
14
            username_host_placeholder: Some(val.username_host_placeholder),
273
14
            disabled_console_color: Some(val.disabled_console_color),
274
14
            highlighted_console_color: Some(val.highlighted_console_color),
275
14
        };
276
14
    }
277
}
278
279
/// Representation of the `daemon` subcommand configurations.
280
#[derive(Serialize, Deserialize, PartialEq, Debug)]
281
pub struct DaemonConfig {
282
    /// Height in pixel of the daemon console window.
283
    ///
284
    /// Note: we are [DPI Unaware][1] which means the number of pixels
285
    ///       represents the `logical` scale, not the physical.
286
    ///
287
    /// [1]: https://learn.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows#dpi-unaware
288
    pub height: i32,
289
    /// Controls how the client console windows make use of the available screen space.
290
    ///
291
    /// * `> 0.0` - Aims for vertical rectangle shape.
292
    ///             The larger the value, the more exaggerated the "verticality".
293
    ///             Eventually the windows will all be columns.
294
    /// * `= 0.0` - Aims for square shape.
295
    /// * `< 0.0` - Aims for horizontal rectangle shape.
296
    ///             The smaller the value, the more exaggerated the "horizontality".
297
    ///             Eventually the windows will all be rows.
298
    ///             `-1.0` is the sweetspot for mostly preserving a 16:9 ratio.
299
    #[serde(alias = "aspect_ratio_adjustement")]
300
    pub aspect_ratio_adjustment: f64,
301
    /// Controls back- and foreground colors of the daemon console window.
302
    ///
303
    /// All [standard Windows color combinations][1] are available:
304
    ///
305
    /// FOREGROUND_BLUE:        1   \
306
    /// FOREGROUND_GREEN:       2   \
307
    /// FOREGROUND_RED:         4   \
308
    /// FOREGROUND_INTENSITY:   8   \
309
    /// BACKGROUND_BLUE:        16  \
310
    /// BACKGROUND_GREEN:       32  \
311
    /// BACKGROUND_RED:         64  \
312
    /// BACKGROUND_INTENSITY:   128 \
313
    ///
314
    /// # Example
315
    ///
316
    /// White font on red background: 8 + 4 + 2 + 1 + 128 + 64 = `207`
317
    ///
318
    /// [1]: https://learn.microsoft.com/en-us/windows/console/console-screen-buffers#character-attributes
319
    pub console_color: u16,
320
    /// Behavior when an arrow / `hjkl` keystroke would move the
321
    /// enable/disable submenu's selection past the edge of the client grid.
322
    ///
323
    /// * `clamp` (default) - keep the current selection.
324
    /// * `wrap` - wrap to the opposite edge of the same row (Left/Right)
325
    ///   or column (Up/Down).
326
    pub submenu_edge_behavior: EdgeBehavior,
327
}
328
329
impl Default for DaemonConfig {
330
    /// Returns a sensible default `DaemonConfig`.
331
    ///
332
    /// # Returns
333
    ///
334
    /// `DaemonConfig` with the following values:
335
    /// * `height`                      - `200`
336
    /// * `aspect_ratio_adjustment`    - `-1.0`
337
    /// * `console_color`               - `207`
338
    /// * `submenu_edge_behavior`       - `clamp`
339
79
    fn default() -> Self {
340
79
        return DaemonConfig {
341
79
            height: 200,
342
79
            aspect_ratio_adjustment: -1f64,
343
79
            console_color: (FOREGROUND_INTENSITY
344
79
                | FOREGROUND_RED
345
79
                | FOREGROUND_GREEN
346
79
                | FOREGROUND_BLUE
347
79
                | BACKGROUND_INTENSITY
348
79
                | BACKGROUND_RED)
349
79
                .0,
350
79
            submenu_edge_behavior: EdgeBehavior::default(),
351
79
        };
352
79
    }
353
}
354
355
/// Representation of the `daemon` subcommand configurations
356
/// where everything is optional.
357
#[derive(Serialize, Deserialize)]
358
pub struct DaemonConfigOpt {
359
    #[allow(missing_docs)]
360
    pub height: Option<i32>,
361
    #[allow(missing_docs)]
362
    #[serde(alias = "aspect_ratio_adjustement")]
363
    pub aspect_ratio_adjustment: Option<f64>,
364
    #[allow(missing_docs)]
365
    pub console_color: Option<u16>,
366
    #[allow(missing_docs)]
367
    pub submenu_edge_behavior: Option<EdgeBehavior>,
368
}
369
370
impl Default for DaemonConfigOpt {
371
12
    fn default() -> Self {
372
12
        return DaemonConfig::default().into();
373
12
    }
374
}
375
376
impl From<DaemonConfigOpt> for DaemonConfig {
377
    /// Unwraps the existing configuration values or applies the default.
378
20
    fn from(val: DaemonConfigOpt) -> Self {
379
20
        let default = DaemonConfig::default();
380
20
        return DaemonConfig {
381
20
            height: val.height.unwrap_or(default.height),
382
20
            aspect_ratio_adjustment: val
383
20
                .aspect_ratio_adjustment
384
20
                .unwrap_or(default.aspect_ratio_adjustment),
385
20
            console_color: val.console_color.unwrap_or(default.console_color),
386
20
            submenu_edge_behavior: val
387
20
                .submenu_edge_behavior
388
20
                .unwrap_or(default.submenu_edge_behavior),
389
20
        };
390
20
    }
391
}
392
393
impl From<DaemonConfig> for DaemonConfigOpt {
394
    /// Wraps all configuration values as options.
395
13
    fn from(val: DaemonConfig) -> Self {
396
13
        return DaemonConfigOpt {
397
13
            height: Some(val.height),
398
13
            aspect_ratio_adjustment: Some(val.aspect_ratio_adjustment),
399
13
            console_color: Some(val.console_color),
400
13
            submenu_edge_behavior: Some(val.submenu_edge_behavior),
401
13
        };
402
13
    }
403
}
404
405
#[cfg(test)]
406
#[path = "../tests/utils/test_config.rs"]
407
mod test_config;